// Command Dictionary Module

// import instruction module
import Instr = require("./Instructions");

// import command module
import Cmd = require("./Commands");

// import Robot Karel module
import Robot = require("./RobotKarel");

// import execution engine
import Exec = require("./ExecutionEngine");

import Parser = require("../Parser/CommandParser");

// import localization
import Loc = require("../../UI/Localization");


// command record for saving in the localStorage
class CommandRecord {
    public Name: string;
    public SourceCode: string;
}


// system command representation
export class SystemCommand extends Cmd.Command {
    public Name: string;
    public CmdType: Instr.eInternalCommand;

    public constructor(name: string, cmdType: Instr.eInternalCommand) {
        super();
        this.Name = name;
        this.CmdType = cmdType;
    }
}

// the class itself
export class Dictionary {

    // singleton instance
    private static mInstance: Dictionary = null;

    // command list
    private mUserCommands: Array<Cmd.UserCommand>;
    private mSystemCommands: Array<SystemCommand>;

    // references
    private mKarel: Robot.RobotKarel;
    private mExecutionEngine: Exec.ExecutionEngine;
    private mLocalization: Loc.Localization;

    private mRedrawEvent: Exec.ICityRedrawRequested;
    private mKarelExceptionEvent: Exec.IKarelExceptionHandler;
    private mExecutionCompleted: Exec.ICommandCompleted;

    // constructor
    public constructor() {
        this.mSystemCommands = new Array<SystemCommand>();
        this.mUserCommands = new Array<Cmd.UserCommand>();
        this.mRedrawEvent = null;
        this.mKarelExceptionEvent = null;
        this.mExecutionCompleted = null;
        this.mLocalization = Loc.Localization.Instance;

        this.mKarel = Robot.RobotKarel.Instance;
        this.mExecutionEngine = Exec.ExecutionEngine.Instance;

        // set exception handlers
        var that = this;

        this.mExecutionEngine.SetRedrawHandler(function () {
            if (that.mRedrawEvent != null) {
                that.mRedrawEvent();
            }
        });

        this.mExecutionEngine.SetKarelExceptionHandler(function (type) {
            if (that.mKarelExceptionEvent != null) {
                that.mKarelExceptionEvent(type);
            }
        });

        this.mExecutionEngine.SetExecutionCompleted(function () {
            if (that.mExecutionCompleted != null) {
                that.mExecutionCompleted();
            }
        });
    }

    // returns the localized command
    private GetCmd(key: string): string {
        return this.mLocalization.Get(key).toLocaleUpperCase();
    }

    // set the redraw event
    public SetRedrawHandler(handler: Exec.ICityRedrawRequested) {
        this.mRedrawEvent = handler;
    }

    // set Karel's exception handler
    public SetKarelExceptionHandler(handler: Exec.IKarelExceptionHandler) {
        this.mKarelExceptionEvent = handler;
    }

    // set execution completed event handler
    public SetExecutionCompleted(handler: Exec.ICommandCompleted) {
        this.mExecutionCompleted = handler;
    }

    // property getter for singleton instance
    public static get Instance(): Dictionary {
        if (Dictionary.mInstance == null)
            Dictionary.mInstance = new Dictionary();
        return Dictionary.mInstance;
    }

    // returns true, when there are no user commands
    public IsDictionaryEmpty(): boolean {
        return this.mUserCommands.length == 0;
    }

    // removes all user commands from the dictionary
    public ClearDictionary(): void {
        // allocate a new user command array
        this.mUserCommands = new Array<Cmd.UserCommand>();
        
        // save the empty dictionary
        this.Save();
    }

    // returns the source code of all commands
    public ExportDictionary(): string {
        // init exported variable
        var exportedSource = "";

        for (var i = 0; i < this.mUserCommands.length; i++) {
            var cmd = this.mUserCommands[i];
            // add the source code
            exportedSource += cmd.GetSourceCode().toLocaleUpperCase();

            // add separator if not the last command
            if (i < this.mUserCommands.length - 1) {
                exportedSource += "\r\n*\r\n";
            }
        }

        return exportedSource;
    }

    // imports the dictionary
    public ImportDictionary(sourceCode: string): void {
        try {
            // split the complete source into command sources
            var commandSources = sourceCode.split("*");

            // parse the commands
            for (var i = 0; i < commandSources.length; i++) {
                // get the source of one command
                var src = commandSources[i].trim();

                // init the parser
                var parser = new Parser.CommandParser(src);
                
                // parse the code
                if (parser.Parse() == true) {
                    if (parser.FinalCommand != null) {
                        this.AddUserCommand(<Cmd.UserCommand>parser.FinalCommand);
                    }
                }
            }
        }
        catch (e) {
        }

        // finally save the dictioanary to the local storage
        this.Save();
    }

    // checks, whether it is a system command
    public IsSystemCommand(commandName: string) {
        return this.GetSystemCommandByName(commandName) != null;
    }

    // checks the existence of the command
    public ExistsUserCommand(name: string): boolean {
        return this.GetUserCommandByName(name) != null;
    }

    // returns the list of commands
    public GetListOfCommands(): Array<string> {
        var arr = new Array<string>();

        // get the system commands
        for (var i = 0; i < this.mSystemCommands.length; i++) {
            arr.push(this.mSystemCommands[i].Name);
        }

        // get the user commands
        for (var i = 0; i < this.mUserCommands.length; i++) {
            arr.push(this.mUserCommands[i].GetName());
        }  

        return arr;
    }

    // loads and parses the commands from the localStorage
    public Load(): boolean {

        // prepare system commands
        this.PrepareSystemCommands();

        try {
            // get the saved commands
            var jsonRec = window.localStorage["Commands"];
            if (jsonRec == undefined || jsonRec == null)
                return false;

            // decrypt the commands
            var commandArray: Array<CommandRecord> = JSON.parse(jsonRec);
            if (commandArray == null || commandArray == undefined)
                return false;

            // parse the commands
            for (var i = 0; i < commandArray.length; i++) {
                var cmdRec = commandArray[i];

                // init the parser
                var parser = new Parser.CommandParser(cmdRec.SourceCode);
                // parse the code
                if (parser.Parse() == true) {
                    if (parser.FinalCommand != null) {
                        this.AddUserCommand(<Cmd.UserCommand>parser.FinalCommand);
                    }
                }                
            }

        }
        catch(e)
        {
            return false;
        }

        return true;
    }


    // saves the commands
    public Save(): void {
        // init the container
        var commandArray = new Array<CommandRecord>();

        // create command records
        for (var i = 0; i < this.mUserCommands.length; i++) {
            var cmd = this.mUserCommands[i];

            var cmdRec = new CommandRecord();
            cmdRec.Name = cmd.GetName();
            cmdRec.SourceCode = cmd.GetSourceCode();

            commandArray.push(cmdRec);
        }

        // save to the storage      
        window.localStorage["Commands"] = JSON.stringify(commandArray);
    }

    // runs the command
    public RunCommand(name: string): boolean {
        // check first system commands
        var systemCmd = this.GetSystemCommandByName(name);
        if (systemCmd != null) {
            // system command found, do it
            this.DoSystemCommmand(systemCmd.CmdType);
            return true;         
        }

        // try to find the user command
        var userCmd = this.GetUserCommandByName(name);
        if (userCmd == null) {
            return false;
        }

        // reset execution engine
        this.mExecutionEngine.Reset();

        // compile the command
        var instrSet = userCmd.CompileCommand(this.mExecutionEngine);

        // run the command
        this.mExecutionEngine.RunCommand(name);
        return true;
    }

    // does the system command
    private DoSystemCommmand(type: Instr.eInternalCommand) {
        switch (type) {
            case Instr.eInternalCommand.Move:
                if (this.mKarel.CanMove()) {
                    this.mKarel.Move();
                }
                else if (this.mKarelExceptionEvent != null) {
                    this.mKarelExceptionEvent(Instr.eKarelException.CannotMove);
                }
                break;
            case Instr.eInternalCommand.TurnLeft:
                this.mKarel.TurnLeft();
                break;
            case Instr.eInternalCommand.Put:
                if (this.mKarel.CanPutMark()) {
                    this.mKarel.PutMark();
                } else if (this.mKarelExceptionEvent != null) {
                        this.mKarelExceptionEvent(Instr.eKarelException.TooManyMarks);
                }               
                break;
            case Instr.eInternalCommand.Pick:
                if (this.mKarel.CanPickMark()) {
                    this.mKarel.PickMark();
                } else if (this.mKarelExceptionEvent != null) {
                    this.mKarelExceptionEvent(Instr.eKarelException.NoMark);
                }
                break;
        }
    }

    // adds the user command
    public AddUserCommand(cmd: Cmd.UserCommand): boolean {
        if (this.ExistsUserCommand(cmd.GetName()) == true)
            return false;
        this.mUserCommands.push(cmd);
        return true;
    }

    // updates the command
    public UpdateCommand(cmdOld: Cmd.UserCommand, cmdNew: Cmd.UserCommand): boolean {
        // if the name has changed
        if (cmdOld.GetName().toLocaleLowerCase() != cmdNew.GetName().toLocaleLowerCase()) {
            // check if the old command is in use
            if (this.IsCommandInUse(cmdOld.GetName()) == true) {
                return false;
            }
        }

        // get index of the old command
        var index = this.mUserCommands.indexOf(cmdOld);
        if (index < 0)
            return false;

        // update the command
        this.mUserCommands[index] = cmdNew;

        return true;
    }


    // removes the specific command
    public DeleteCommand(commandName: string): boolean {
        // check if command not in use
        if (this.IsCommandInUse(commandName) == true)
            return false;

        // get the command
        var cmdToDel = this.GetUserCommandByName(commandName);
        if (cmdToDel == null)
            return false;

        // get its index
        var idx = this.mUserCommands.indexOf(cmdToDel);      

        // remove the command
        if (idx >= 0) {
            this.mUserCommands.splice(idx, 1);
        }

        return true;
    }

    // checks whether this command is a child command of another - meaning it cannot be deleted
    public IsCommandInUse(commandName: string): boolean {
        // iterate all user commands
        for (var i = 0; i < this.mUserCommands.length; i++) {
            if (this.HasDependencyOnCommand(this.mUserCommands[i], commandName) == true)
                return true;
        }

        // return no dependency
        return false;
    }

    // checks whether the user command is dependent on another
    private HasDependencyOnCommand(cmd: Cmd.UserCommand, commandName: string): boolean {
        // check if we have the same command
        if (cmd.GetName().toLocaleLowerCase() == commandName.toLocaleLowerCase())
            return false;

        // iterate all child commands
        for (var i = 0; i < cmd.ChildCommands.length; i++) {
            if (this.CheckForCallCommand(cmd.ChildCommands[i], commandName) == true)
                return true;
        }

        return false;
    }

    // traverses the command tree for checking call command with specified name
    private CheckForCallCommand(cmd: Cmd.Command, commandName: string): boolean {
        // check the user command
        if (cmd instanceof Cmd.UserCommand) {
            var userCommand = <Cmd.UserCommand>cmd;
            // check the name of the current command
            if (userCommand.GetName().toLocaleLowerCase() == commandName.toLocaleLowerCase())
                return true;
        }
        // check the command block
        else if (cmd instanceof Cmd.CommandBlock) {
            // check all commands
            var cmdBlock = <Cmd.CommandBlock>cmd;

            // iterate all child commands
            for (var i = 0; i < cmdBlock.ChildCommands.length; i++) {
                if (this.CheckForCallCommand(cmdBlock.ChildCommands[i], commandName) == true)
                    return true;
            }
        }
        // check IF statement
        else if (cmd instanceof Cmd.IfCommand) {
            var ifCmd = <Cmd.IfCommand>cmd;
            // check THEN block
            if (this.CheckForCallCommand(ifCmd.ThenBlock, commandName) == true)
                return true;
            // check ELSE block
            if (ifCmd.ElseBlock != null) {
                if (this.CheckForCallCommand(ifCmd.ElseBlock, commandName) == true)
                    return true;
            }
        }

        return false;
    }

    // return user command by name
    public GetUserCommandByName(name: string): Cmd.UserCommand {
        name = name.toLocaleLowerCase();
        for (var i = 0; i < this.mUserCommands.length; i++) {
            var cmd = this.mUserCommands[i];
            if (cmd.GetName().toLocaleLowerCase() == name)
                return cmd;
        }

        return null;
    }

    // get system command by name
    private GetSystemCommandByName(name: string): SystemCommand {
        name = name.toLocaleLowerCase();
        for (var i = 0; i < this.mSystemCommands.length; i++) {
            var cmd = this.mSystemCommands[i];
            if (cmd.Name.toLocaleLowerCase() == name)
                return cmd;
        }

        return null;
    }

    // prepares the system commands
    private PrepareSystemCommands() {
        this.mSystemCommands.push(new SystemCommand(this.GetCmd("cmdStep"), Instr.eInternalCommand.Move));
        this.mSystemCommands.push(new SystemCommand(this.GetCmd("cmdTurnLeft"), Instr.eInternalCommand.TurnLeft));
        this.mSystemCommands.push(new SystemCommand(this.GetCmd("cmdPut"), Instr.eInternalCommand.Put));
        this.mSystemCommands.push(new SystemCommand(this.GetCmd("cmdPick"), Instr.eInternalCommand.Pick));
    }
}
